package com.oriole.common.util;

import com.oriole.common.constant.Constants;
import com.oriole.common.exception.OrioleRuntimeException;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;

/**
 * Created by IntelliJ IDEA.
 *
 * @author doublelife
 * Email: hautxxxyzjk@163.com
 * Date: 2019/6/8
 * Time: 21:32
 * Description:
 *
 * linux直接生成：JSEncrypt支持的是openssl生成的pkcs1格式私钥，java需要pkcs8格式私钥，公钥格式不变
 * 公私钥生成，【私钥】格式转换
 * 生成公私钥 openssl genrsa -out rsa_1024_priv.pem 1024&& openssl rsa -pubout -in rsa_1024_priv.pem -out rsa_1024_pub.pem
 * 或者http://travistidwell.com/jsencrypt/demo/index.html 在线生成
 *
 * 上面生成的是pkcs1,java需要pkcs8，私钥要转为pkcs8格式
 * pkcs1 转pkcs8  openssl pkcs8 -topk8 -inform PEM -in private.key -outform pem -nocrypt -out pkcs8.pem
 * pkcs8 转pkcs1  openssl rsa -in pkcs8.pem -out pri_key.pem
 */
public class RsaUtils {

    public static Map<String, String> createKeys(int keySize) {
        //为RSA算法创建一个KeyPairGenerator对象
        KeyPairGenerator kpg;
        try {
            kpg = KeyPairGenerator.getInstance(Constants.RSA_ALGORITHM);
        } catch (NoSuchAlgorithmException e) {
            throw new OrioleRuntimeException("No such algorithm-->[" + Constants.RSA_ALGORITHM + "]");
        }
        //初始化KeyPairGenerator对象,密钥长度
        kpg.initialize(keySize);
        //生成密匙对
        KeyPair keyPair = kpg.generateKeyPair();
        //得到公钥
        Key publicKey = keyPair.getPublic();
        String publicKeyStr = Base64.encodeBase64URLSafeString(publicKey.getEncoded());
        //得到私钥
        Key privateKey = keyPair.getPrivate();
        System.out.println("private key format is :" + privateKey.getFormat());
        String privateKeyStr = Base64.encodeBase64URLSafeString(privateKey.getEncoded());
        Map<String, String> keyPairMap = new HashMap<>(8);
        keyPairMap.put("publicKey", publicKeyStr);
        keyPairMap.put("privateKey", privateKeyStr);
        return keyPairMap;
    }

    public static RSAPublicKey getPublicKey(String publicKey) {
        //通过X509编码的Key指令获得公钥对象
        KeyFactory keyFactory;
        try {
            keyFactory = KeyFactory.getInstance(Constants.RSA_ALGORITHM);
        } catch (NoSuchAlgorithmException e) {
            throw new OrioleRuntimeException("No such algorithm-->[" + Constants.RSA_ALGORITHM + "]");
        }
        X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKey));
        try {
            return (RSAPublicKey) keyFactory.generatePublic(x509KeySpec);
        } catch (InvalidKeySpecException e) {
            throw new OrioleRuntimeException("Invalid Key SpecException-->[" + publicKey + "]");
        }
    }

    public static RSAPrivateKey getPrivateKey(String privateKey) {
        //通过PKCS#8编码的Key指令获得私钥对象
        KeyFactory keyFactory;
        try {
            keyFactory = KeyFactory.getInstance(Constants.RSA_ALGORITHM);
        } catch (NoSuchAlgorithmException e) {
            throw new OrioleRuntimeException("No such algorithm-->[" + Constants.RSA_ALGORITHM + "]");
        }
        PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey));
        try {
            return (RSAPrivateKey) keyFactory.generatePrivate(pkcs8KeySpec);
        } catch (InvalidKeySpecException e) {
            throw new OrioleRuntimeException("Invalid Key SpecException-->[" + privateKey + "]");
        }
    }

    /**
     * Encrypt data by public key
     *
     * @param secret    Data to be encrypted
     * @param publicKey publicKey
     * @return String
     */
    public static String publicEncrypt(String secret, RSAPublicKey publicKey) {
        try {
            Cipher cipher = Cipher.getInstance(Constants.RSA_ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, publicKey);
            byte[] secretBytes = secret.getBytes(Constants.CHARSET);
            int keySize = publicKey.getModulus().bitLength();
            byte[] bytes = rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, secretBytes, keySize);
            return Base64.encodeBase64URLSafeString(bytes);
        } catch (NoSuchPaddingException | NoSuchAlgorithmException | UnsupportedEncodingException | InvalidKeyException e) {
            throw new OrioleRuntimeException("Encrypt data by public key Exception-->[" + e + "]");
        }
    }

    /**
     * Decrypt data by private key
     *
     * @param secret     Data to be decrypted
     * @param privateKey privateKey
     * @return String
     */
    public static String privateDecrypt(String secret, RSAPrivateKey privateKey) {
        try {
            Cipher cipher = Cipher.getInstance(Constants.RSA_ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, privateKey);
            byte[] secretBytes = Base64.decodeBase64(secret);
            int keySize = privateKey.getModulus().bitLength();
            byte[] bytes = rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, secretBytes, keySize);
            return new String(bytes, Constants.CHARSET);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | UnsupportedEncodingException e) {
            throw new OrioleRuntimeException("Decrypt data by private key Exception-->[" + e + "]");
        }
    }

    /**
     * Encrypt data by private key
     *
     * @param secret     Data to be encrypted
     * @param privateKey privateKey
     * @return String
     */
    public static String privateEncrypt(String secret, RSAPrivateKey privateKey) {
        try {
            Cipher cipher = Cipher.getInstance(Constants.RSA_ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, privateKey);
            byte[] secretBytes = secret.getBytes(Constants.CHARSET);
            int keySize = privateKey.getModulus().bitLength();
            byte[] bytes = rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, secretBytes, keySize);
            return Base64.encodeBase64URLSafeString(bytes);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | UnsupportedEncodingException e) {
            throw new OrioleRuntimeException("Encrypt data by private key Exception-->[" + e + "]");
        }
    }

    /**
     * Decrypt data by public key
     *
     * @param secret     Data to be decrypted
     * @param publicKey publicKey
     * @return String
     */
    public static String publicDecrypt(String secret, RSAPublicKey publicKey) {
        try {
            Cipher cipher = Cipher.getInstance(Constants.RSA_ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, publicKey);
            byte[] secretBytes = Base64.decodeBase64(secret);
            int keySize = publicKey.getModulus().bitLength();
            byte[] bytes = rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, secretBytes, keySize);
            return new String(bytes, Constants.CHARSET);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | UnsupportedEncodingException e) {
            throw new OrioleRuntimeException("Decrypt data by public key Exception-->[" + e + "]");
        }
    }

    private static byte[] rsaSplitCodec(Cipher cipher, int decryptMode, byte[] secretBytes, int keySize) {
        int maxBlock = 0;
        if (decryptMode == Cipher.DECRYPT_MODE) {
            maxBlock = keySize / 8;
        } else {
            maxBlock = keySize / 8 - 11;
        }
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int offSet = 0;
        byte[] buff;
        int i = 0;
        try {
            while (secretBytes.length > offSet) {
                if (secretBytes.length - offSet > maxBlock) {
                    buff = cipher.doFinal(secretBytes, offSet, maxBlock);
                } else {
                    buff = cipher.doFinal(secretBytes, offSet, secretBytes.length - offSet);
                }
                out.write(buff, 0, buff.length);
                i++;
                offSet = i * maxBlock;
            }
            return out.toByteArray();
        } catch (IllegalBlockSizeException | BadPaddingException e) {
            throw new OrioleRuntimeException("Encryption and decryption threshold Exception-->[" + e + "]");
        } finally {
            IOUtils.closeQuietly(out);
        }
    }

    public static void main(String[] args) {
        Map<String, String> keys = RsaUtils.createKeys(1024);
        String publicKey = keys.get("publicKey");
        String privateKey = keys.get("privateKey");

        String encodedData = RsaUtils.publicEncrypt("123456", RsaUtils.getPublicKey(publicKey));
        String decodedData = RsaUtils.privateDecrypt(encodedData, RsaUtils.getPrivateKey(privateKey));
        System.out.println("decrypted secret is: " + decodedData);

        String encodedData1 = RsaUtils.privateEncrypt("1234567", RsaUtils.getPrivateKey(privateKey));
        String decodedData1 = RsaUtils.publicDecrypt(encodedData1, RsaUtils.getPublicKey(publicKey));
        System.out.println("decrypted secret is: " + decodedData1);
    }

}
